/*
 * MIT License
 *
 * Copyright (c) 2021 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.seaboot.excel;

import cn.seaboot.commons.core.Converter;
import cn.seaboot.html.table.TD;
import org.apache.poi.ss.usermodel.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.core.convert.ConversionService;

import java.math.BigDecimal;
import java.time.LocalTime;
import java.util.Date;

/**
 * Excel单元格处理器，封装了单元格数据的读写。
 * <p>
 * POI 序号问题：sheet 从 0 开始， row 从 0 开始，column 从 0 开始；
 * 如果修改过 sheet，序号可能从 1 开始，但是新建的肯定是从 0 开始。
 * <p>
 * 模版的用法：
 * 基于Excel模版，模版Excel需要有标题和一行样例数据，
 * 数据导出的时候，以这一行数据作为模版，为新增的每一行数据，添加相同的样式。
 * <p>
 * 特殊单元格式还需进一步确认，暂未深入研究：CELL_TYPE_FORMULA
 * <p>
 *
 * <p>Cell.CELL_TYPE_NUMERIC    数值类型：整数、小数、日期，与Excel的单元格格式化有关
 * <p>Cell.CELL_TYPE_STRING     字符串
 * <p>Cell.CELL_TYPE_FORMULA    公式，如果 Excel 文件已保存公式的最后计算结果（即文件保存时 Excel 自动计算的结果），可以直接读取单元格的缓存值，而无需 FormulaEvaluator：
 * <p>Cell.CELL_TYPE_BLANK      空的单元格，合并单元格等，有单元格样式
 * <p>Cell.CELL_TYPE_BOOLEAN    布尔值
 * <p>Cell.CELL_TYPE_ERROR      错误单元格
 * <p>
 * <p>@date 2019/08/02 14:23
 * <p>@date 2020/01/09 09:58  增加查询ListMap的函数
 * <p>@date 2020/06/09 18:09  readString() CELL_TYPE_NUMERIC类型转为BigDecimal之后再转字符串，解决科学计数中有E的情况
 * <p>@date 2020/07/09 18:09  删除全部 write 和 read 代码，采用 fluent 风格，减少代码重写
 *
 * @author Mr.css
 * @see Cell
 * @see ExcelBook
 * @see ConversionService 参数类型转换基于ConversionService，可以直接从spring上下文中获取，按照项目需求调整数据类型
 */
public class ExcelCell {
    private ExcelCell() {
    }

    /**
     * 在单元格一行中，创建一个单元格，主要用于辅助{@link ExcelBook}
     *
     * @param row   行对象
     * @param idx   第几列（从0开始）
     * @param value 值
     * @param style 样式
     * @return Cell instance
     * @see ExcelBook
     */
    public static Cell addCell(Row row, int idx, Object value, @Nullable CellStyle style) {
        Cell cell = row.createCell(idx);
        cell.setCellStyle(style);
        setValue(cell, value);
        return cell;
    }

    /**
     * 在单元格一行中，创建一个单元格，并添加数据
     *
     * @param row   行对象
     * @param idx   第几列（从0开始）
     * @param value 值
     * @return Cell instance
     * @see ExcelBook
     */
    public static Cell addCell(Row row, int idx, Object value) {
        Cell cell = row.createCell(idx);
        setValue(cell, value);
        return cell;
    }

    /**
     * 转字符串后填充到单元格
     *
     * @param cell  cell
     * @param value value
     * @param clazz 数据类型，class只支持[String/Number/Date/Boolean]
     */
    public static void setValue(Cell cell, Object value, Class<?> clazz) {
        setValue(cell, Converter.convert(value, clazz));
    }

    /**
     * 此函数保证了数据以最适合的格式导出
     * <p>
     * 日期类型：
     * 日期类型会被POI转换成数字类型，需要在样式行，设置单元格为日期格式
     * 如果不希望程序处理，可以手动转为字符串
     * <p>
     * 数字类型：
     * Excel单元格所能放置的数字，是有数值上限的，十进制数十二位左右（整数 + 小数）；
     * 在处理高精度 double、long 类型的数据时，最好手动转为字符串。
     *
     * @param cell  单元格
     * @param value 数值
     */
    public static void setValue(Cell cell, Object value) {
        if (value == null) {
            // maybe ignore is better choice
            cell.setCellValue("");
        } else if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof Long) {
            // 像是手机号这类的长数字，转成 Double 之后，显示时会转为科学计数法，这里默认处理成 String 类型
            cell.setCellValue(Converter.convert(value, String.class));
        } else if (value instanceof Double) {
            cell.setCellValue((Double) value);
        } else if (value instanceof Number) {
            cell.setCellValue(((Number) value).doubleValue());
        } else if (value instanceof Date) {
            cell.setCellValue((Date) value);
        } else if (value instanceof Boolean) {
            cell.setCellValue((boolean) value);
        } else {
            cell.setCellValue(Converter.convert(value, String.class));
        }
    }

    /**
     * 转字符串后填充到单元格
     *
     * @param cell  cell
     * @param value value
     */
    public static void setValueAsString(Cell cell, Object value) {
        cell.setCellValue(Converter.convert(value, String.class));
    }

    /**
     * 处理异常单元格
     *
     * @param cell cell
     */
    public static void invalidCell(Cell cell) {
        throw new ExcelException("Excel cell is invalid: [" + cell.getRowIndex() + " , " + cell.getColumnIndex() + "]");
    }

    /**
     * 读取数据为字符串，
     * 注意事项：
     * 1、所有的数字类型，读取到的时候是Double类型，此类型转为字符串会包含小数点，
     * 2、日期类型一律转换为DateTime格式。
     *
     * @param cell 单元格
     * @return 值
     */
    public static String readString(Cell cell) {
        if (cell != null) {
            switch (cell.getCellType()) {
                case STRING:
                    return cell.getStringCellValue();
                case BOOLEAN:
                    return Boolean.toString(cell.getBooleanCellValue());
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        // 日期类型
                        Date date = DateUtil.getJavaDate(cell.getNumericCellValue());
                        return Converter.convert(date, String.class);
                    } else {
                        // 数字类型
                        return BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString();
                    }
                case FORMULA:
                    // 这种单元格未在业务中使用，可能存在问题
//                    return cell.getCellFormula();
                    return String.valueOf(cell.getNumericCellValue());
                case BLANK:
                    return null;
                default:
                    // 未知的单元格类型
                    invalidCell(cell);
                    break;
            }
        }
        return null;
    }

    /**
     * 使用默认的数据转换方式 封装数据
     *
     * @param cell cell 单元格未填写数据的时候，获取的cell是null，因此这里也不会抛出空指针异常，而是返回null
     * @return value
     */
    public static Object readValue(Cell cell) {
        if (cell != null) {
            switch (cell.getCellType()) {
                case STRING:
                    return cell.getStringCellValue();
                case BOOLEAN:
                    return cell.getBooleanCellValue();
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        return DateUtil.getJavaDate(cell.getNumericCellValue());
                    } else {
                        return cell.getNumericCellValue();
                    }
                case FORMULA:
//                    return cell.getCellFormula();
                    return cell.getNumericCellValue();
                case BLANK:
                    return null;
                default:
                    invalidCell(cell);
                    break;
            }
        }
        return null;
    }

    /**
     * 读取数据为字符串，
     * 注意事项：
     * 1、所有的数字类型，读取到的时候是Double类型，此类型转为字符串会包含小数点，
     * 2、日期类型一律转换为DateTime格式。
     * <p>
     * 使用 {@link FormulaEvaluator} 读取单元格公式，结果更加可靠，但是读取稍慢，
     *
     * @param evaluator 公式评估
     * @param cell      单元格
     * @return 值
     */
    public static String readString2(FormulaEvaluator evaluator, Cell cell) {
        if (cell != null) {
            switch (cell.getCellType()) {
                case STRING:
                    return cell.getStringCellValue();
                case BOOLEAN:
                    return Boolean.toString(cell.getBooleanCellValue());
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        // 日期类型
                        Date date = DateUtil.getJavaDate(cell.getNumericCellValue());
                        return Converter.convert(date, String.class);
                    } else {
                        // 数字类型
                        return BigDecimal.valueOf(cell.getNumericCellValue()).toPlainString();
                    }
                case FORMULA:
                    return Converter.toString(readFormulaValue(evaluator, cell));
                case BLANK:
                    return null;
                default:
                    // 未知的单元格类型
                    invalidCell(cell);
                    break;
            }
        }
        return null;
    }

    /**
     * 使用默认的数据转换方式 封装数据
     * <p>
     * 使用 {@link FormulaEvaluator} 读取单元格公式，结果更加可靠，但是读取稍慢，
     *
     * @param evaluator 公式评估
     * @param cell      cell 单元格未填写数据的时候，获取的cell是null，因此这里也不会抛出空指针异常，而是返回null
     * @return value
     */
    public static Object readValue2(FormulaEvaluator evaluator, Cell cell) {
        if (cell != null) {
            switch (cell.getCellType()) {
                case STRING:
                    return cell.getStringCellValue();
                case BOOLEAN:
                    return cell.getBooleanCellValue();
                case NUMERIC:
                    if (DateUtil.isCellDateFormatted(cell)) {
                        return DateUtil.getJavaDate(cell.getNumericCellValue());
                    } else {
                        return cell.getNumericCellValue();
                    }
                case FORMULA:
                    return readFormulaValue(evaluator, cell);
                case BLANK:
                    return null;
                default:
                    invalidCell(cell);
                    break;
            }
        }
        return null;
    }

    /**
     * 读取单元格中公式的计算结果
     * <p>
     * 使用 {@link FormulaEvaluator} 读取单元格公式，结果更加可靠，但是读取稍慢，
     * <p>
     * 一般不启用当前函数：
     * 1. 如果文件中已经保存了计算结果，可以直接读取（一般可以直接读）；
     * 2. 日常业务中，可以直接要求客户另存计算结果；
     * 3. 与 StreamingReader 不兼容，大型文件还是得要求用户另存文件。
     *
     * @param cell 单元格
     * @return 计算结果
     */
    public static Object readFormulaValue(@NotNull FormulaEvaluator evaluator, @NotNull Cell cell) {
        CellValue cv = evaluator.evaluate(cell);
        switch (cv.getCellType()) {
            case NUMERIC:
                return cv.getNumberValue();
            case STRING:
                return cv.getStringValue();
            case BOOLEAN:
                return cv.getBooleanValue();
            case ERROR:
                // 公式错误
                return cv.getErrorValue();
            default:
                return "unknown cell type: " + cv.getCellType();
        }
    }

    /**
     * 读取数据，并且按照类型转换格式
     *
     * @param cell   cell
     * @param target 类型
     * @return value
     */
    public static <T> T readValue(Cell cell, Class<T> target) {
        Object res = readValue(cell);
        if (res != null) {
            return Converter.convert(res, target);
        } else {
            return null;
        }
    }

    /**
     * Excel时间格式：将hh:mm:ss格式的时间转换成0-1的小数(24小时的占比)
     *
     * @param time Excel单元格内读取到的时间
     */
    public static LocalTime parseTime(double time) {
        double ret = 24 * 60 * 60 * time;
        int h = (int) ret / 3600;
        ret = ret - h * 3600;
        int m = (int) ret / 60;
        int s = (int) (ret - m * 60) / 60;
        return LocalTime.of(h, m, s);
    }

    /**
     * Excel中时间格式用的是微软时间，从1900年开始计算天数，java从1970年开始，二者时间相差25570天
     * 25570由来："2019-11-15"，微软天数为43784，java天数为18214，43784 - 18214 = 25570
     *
     * @param date Excel单元格内读取到的日期
     */
    public static Date parseDate(Double date) {
        return new Date((date.longValue() - 25569) * 86400000L);
    }

    /**
     * 读取单元格关键信息
     *
     * @param sheet 工作表 用于获取列宽
     * @param cell  单元格
     * @return -
     */
    public static TD getTableData(Sheet sheet, Cell cell) {
        TD ret = new TD();

        ret.setValue(ExcelCell.readString(cell));
        ret.setWidth(sheet.getColumnWidth(cell.getColumnIndex()));

        CellStyle style = cell.getCellStyle();

        // 背景色
        Color color = style.getFillForegroundColorColor();
        if (color != null) {
            ret.setBackgroundColor(ExcelStyle.convertToAwtColor(color));
        }

        // 对齐方式
        ret.setAlign(style.getAlignment());
        ret.setValign(style.getVerticalAlignment());

        return ret;
    }
}
